/*
 * Accessor.cs
 * 
 * Microsoft Office SharePoint Server Managed Protocol Handler
 * 
 * Author: John Kozell (johnkoz@microsoft.com)
 *         Microsoft Coroporation
 * 
 * This code is an adaptation from, and interpretation of, the original SPS 2001 SDK
 * and has been updated with available information at the time of writing.
 * Not all the delcarions and alogrithms have been verified and tested
 * and there is no guarantee that they are accurate or valid, now or in the future.
 * 
 * The .NET Managed COM Interop approach contained herin was NOT created in cooperation
 * with the Microsoft SharePoint product group and has not been officially endorced
 * by Microsoft and therefore is not supported. The user of this code assumes any
 * and all risk with its use and should proceed with discression.
 * 
 * THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
 * PARTICULAR PURPOSE.
 */

using System;
using System.Collections;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;

namespace MOSSPH
{
	/// <summary>
	/// Manages the extraction of properties and content from either a Container or an Item.
	/// For a Container, IFilter will emit additional URLs for subsequent reentry.
	/// For an Item, IFilter will emit the actual content and properties of the item.
	/// </summary>
	public class Accessor : IUrlAccessor, IFilter
	{
		private IContentEnumerator m_ContentEnumerator = null;
		private ArrayList m_PropertyChunks = null;
		private ChunkInfo m_CurrentChunk = null;
		private bool m_bDirectory = false;
		private uint m_ulChunkId = 0;
		private uint m_lcid = 1033; // TODO. Get this from somewhere.


		/// <summary>
		/// Uses pce to prepare the accessor for subsequent calls.
		/// </summary>
		/// <param name="pce"></param>
		/// <returns></returns>
		public HRESULT Init(IContentEnumerator pce)
		{
			DateTime enterTime = Logging.Enter(typeof(Accessor), "Init");
			Logging.Information("Accessor::Init(" + pce.Url + ")");
			HRESULT hr = (HRESULT)SCODE.S_OK;
			string sOperation = null;

			try
			{
				m_ContentEnumerator = pce; // Save my copy of the content enumerator.
				if ((m_ContentEnumerator.IsContainer))
				{
					m_bDirectory = true;
				}
				else
				{
					m_bDirectory = false;

					// Download the content for use later.
					m_ContentEnumerator.EnsureContent();

					sOperation = "adding properties";
					// Build property list to chunk-out later.
					m_PropertyChunks = new ArrayList();

					// LaunchUrl
                    m_PropertyChunks.Add(new ChunkInfo(PROPSET_SEARCH.GUID, (uint)PROPSET_SEARCH.PID.URL, m_ContentEnumerator.LaunchUrl));

					// HREF display text.
                    m_PropertyChunks.Add(new ChunkInfo(PROPSET_DOCUMENTS.GUID, (uint)PROPSET_DOCUMENTS.PID.TITLE, m_ContentEnumerator.Title));

                    // Add additional custom properties from the ContentEnumerator.
                    foreach (Property prop in m_ContentEnumerator.Properties)
                    {
                        Logging.Trace(string.Format("Adding property <{0}> {1} = {2}", prop.GUID, prop.Name, prop.Val));
                        m_PropertyChunks.Add(new ChunkInfo(prop.GUID, prop.Name, prop.Val));
                    }
				}
			}
			catch (Exception e)
			{
				hr = HRESULT.E_FAIL;
				Logging.Exception("Accessor::Init (" + sOperation + ")", e);
			}
			Logging.Leave(typeof(Accessor), "Init", hr.ToString(), enterTime);

			return hr;
		}


		/// <summary>
		/// Explicitly cleans up any resources the accessor allocated.
		/// Technically this isn't needed, but old habits die hard.
		/// </summary>
		public HRESULT Close()
		{
            Logging.Enter(typeof(Accessor), "Close");
			try
			{
                m_CurrentChunk = null;
				m_PropertyChunks = null;
				if (m_ContentEnumerator != null)
				{
					m_ContentEnumerator.Terminate();
					m_ContentEnumerator = null;
				}
				m_bDirectory = false;
			}
			catch (Exception e)
			{
				Logging.Exception("Accessor::Close", e);
			}
            Logging.Leave(typeof(Accessor), "Close");

            return (HRESULT)SCODE.S_OK;
        }


		/// <summary>
		/// If asked, returns a reference to the underlying ContentEnumerator used to initialize this accessor.
		/// </summary>
		public IContentEnumerator Content
		{
			get
			{
				return m_ContentEnumerator;
			}
		}


		#region IUrlAccessor Members

		public HRESULT AddRequestParameter(ref PROPSPEC pSpec, ref PROPVARIANT pVar)
		{
			Logging.Called(typeof(Accessor), "AddRequestParameter");

            return HRESULT.E_NOTIMPL;
		}


        public HRESULT GetDocFormat(System.IntPtr wszDocFormat, uint dwSize, out uint pdwLength)
		{
			Logging.Enter(typeof(Accessor),"GetDocFormat");
			HRESULT hr = HRESULT.E_NOTIMPL; 
			uint dwLength = 0;

			try
			{
				if (!m_bDirectory)
				{
					dwLength = (uint)COMInterop.CopyStringToIntPtr(m_ContentEnumerator.ContentType, wszDocFormat);
					hr = (HRESULT)SCODE.S_OK;
				}
			}
			catch (Exception e)
			{
				Logging.Exception("Accessor::GetDocFormat", e);
			}

			pdwLength = dwLength;
			Logging.Leave(typeof(Accessor),"GetDocFormat", hr.ToString());

            return hr;
		}


		public HRESULT GetCLSID(out Guid pClsid)
		{
            Logging.Enter(typeof(Accessor), "GetCLSID");
			HRESULT hr = HRESULT.E_NOTIMPL; 

			pClsid = Guid.Empty;
            Logging.Leave(typeof(Accessor), "GetCLSID", hr.ToString());

            return hr;
		}


		public HRESULT GetHost(System.IntPtr wszHost, uint dwSize, out uint pdwLength)
		{
			Logging.Called(typeof(Accessor), "GetHost");
			pdwLength = 0;

            return HRESULT.E_NOTIMPL;
		}


		/// <summary>
		/// Tells the filter daemon if this piece of content is a container or item.
		/// </summary>
		public HRESULT IsDirectory()
		{
			Logging.Called(typeof(Accessor), "IsDirectory", m_bDirectory.ToString());
			HRESULT hr = (HRESULT)SCODE.S_OK;
			
			if (!m_bDirectory)
				hr = (HRESULT)SCODE.S_FALSE;

            return hr;
		}

        /// <summary>
        ///  Always return a size. For directories it's zero.
        /// </summary>
        /// <param name="pllSize"></param>
        /// <returns></returns>
		public HRESULT GetSize(out ulong pllSize)
		{
			Logging.Enter(typeof(Accessor), "GetSize");
            HRESULT hr = (HRESULT)SCODE.S_OK;
			ulong ulSize = 0;

			if (!m_bDirectory)
			{
				ulSize = m_ContentEnumerator.Size;
				Logging.Information("Size: " + ulSize.ToString());
			}
            pllSize = ulSize;

			Logging.Leave(typeof(Accessor), "GetSize");
            return hr;
		}


        public HRESULT GetLastModified(out FILETIME pftLastModified)
		{
            Logging.Enter(typeof(Accessor), "GetLastModified");
			HRESULT hr = HRESULT.E_NOTIMPL;
			FILETIME ftLastModified = new FILETIME();

			try
			{
				ftLastModified = COMInterop.DateTimeToFileTime(m_ContentEnumerator.LastModified);
				Logging.Trace(m_ContentEnumerator.Url + ", content date: " + m_ContentEnumerator.LastModified.ToString());
				hr = (HRESULT)SCODE.S_OK;
			}
			catch (Exception e)
			{
				Logging.Exception("Accessor::GetLastModified", e);
			}

			pftLastModified = ftLastModified;

            Logging.Leave(typeof(Accessor), "GetLastModified");
            return hr;
		}


		public HRESULT GetFileName(System.IntPtr wszFileName, uint dwSize, out uint pdwLength)
		{
            Logging.Called(typeof(Accessor), "GetFileName");
            HRESULT hr = HRESULT.E_NOTIMPL;
            uint dwLength = 0;

            try
            {
                string filePath = m_ContentEnumerator.FilePath;
                if (!string.IsNullOrEmpty(filePath))
                {
                    dwLength = (uint)COMInterop.CopyStringToIntPtr(filePath, wszFileName);
                    hr = (HRESULT)SCODE.S_OK;
                }
            }
            catch (Exception e)
            {
                Logging.Exception("Accessor::GetDocFormat", e);
            }

            pdwLength = dwLength;
            
            return hr;
		}


		public HRESULT GetSecurityDescriptor(System.IntPtr pSD, uint dwSize, out uint pdwLength)
		{
			Logging.Enter(typeof(Accessor), "GetSecurityDescriptor");
			HRESULT hr = HRESULT.E_NOTIMPL;
			uint dwLength = 0;
			byte[] bDescriptor = m_ContentEnumerator.SecurityDescriptor;

			if (bDescriptor != null && bDescriptor.Length > 0)
			{
				dwLength = (uint)bDescriptor.Length;
				if (dwLength <= dwSize)
				{
					Logging.Information("Returning security descriptor, len: " + dwLength.ToString());
					Marshal.Copy(bDescriptor, 0, pSD, (int)dwLength);
					hr = (HRESULT)SCODE.S_OK;
				}
				else
				{
					hr = HRESULT.ERROR_INSUFFICIENT_BUFFER;
				}
			}
			pdwLength = dwLength;

            Logging.Leave(typeof(Accessor), "GetSecurityDescriptor", hr.ToString());
            return hr;
		}


		public HRESULT GetRedirectedURL(System.IntPtr wszRedirectedUrl, uint dwSize, out uint pdwLength)
		{
            Logging.Called(typeof(Accessor), "GetRedirectedURL");

			pdwLength = 0;
            return HRESULT.E_NOTIMPL;
		}


		public HRESULT GetSecurityProvider(out Guid pSPClsid)
		{
            Logging.Called(typeof(Accessor), "GetSecurityProvider");

			pSPClsid = Guid.Empty;
            return HRESULT.E_NOTIMPL;
        }


		public HRESULT BindToStream(out System.Runtime.InteropServices.ComTypes.IStream ppStream)
		{
            Logging.Enter(typeof(Accessor), "BindToStream");
			HRESULT hr = HRESULT.E_NOTIMPL;
			System.Runtime.InteropServices.ComTypes.IStream stream = null;

			try
			{
				if (!m_bDirectory)
				{
					stream = m_ContentEnumerator.Content;
					if (stream != null)
						hr = (HRESULT)SCODE.S_OK;
				}
			}
			catch (Exception e)
			{
				Logging.Exception("Accessor::BindToStream", e);
			}

			ppStream = stream; // Give the filter daemon back a stream to read the file.

            Logging.Leave(typeof(Accessor), "BindToStream", hr.ToString());
            return hr;
		}


		public HRESULT BindToFilter(out IFilter ppFilter)
		{
            Logging.Called(typeof(Accessor), "BindToFilter");

			// Need a filter for either directory or item.
			ppFilter = this as IFilter;
            return (HRESULT)SCODE.S_OK;
		}

		#endregion


		#region IFilter Members

		public IFilterReturnCodes Init(IFILTER_INIT grfFlags, uint cAttributes, FULLPROPSPEC[] aAttributes, ref uint pFlags)
		{
			Logging.Enter(typeof(Accessor), "Init");

			while (cAttributes > 0)
			{
				--cAttributes;
				Logging.Trace("Guid: " + aAttributes[cAttributes].guidPropSet.ToString());
				Logging.Trace("PRSPEC: " + aAttributes[cAttributes].psProperty.ulKind.ToString("D6"));
				switch (aAttributes[cAttributes].psProperty.ulKind)
				{
					case PRSPEC.PROPID:
						Logging.Trace("PropID: " + aAttributes[cAttributes].psProperty.union.propid.ToString());
						break;
					case PRSPEC.LPWSTR:
						Logging.Trace("String: " + aAttributes[cAttributes].psProperty.union.lpwstr);
						break;
				}
			}
            Logging.Leave(typeof(Accessor), "Init");
            return IFilterReturnCodes.S_OK;
		}
		

		public IFilterReturnCodes GetChunk(ref STAT_CHUNK pStat)
		{
            Logging.Enter(typeof(Accessor), "GetChunk");

			IFilterReturnCodes hr = IFilterReturnCodes.FILTER_E_END_OF_CHUNKS;
			pStat = new STAT_CHUNK();

            // First, chunk out properties for this container/item.
            if (m_PropertyChunks != null && m_PropertyChunks.Count > 0)
            {
                // Pop a value off the collection.
                m_CurrentChunk = (ChunkInfo)m_PropertyChunks[m_PropertyChunks.Count - 1];
                m_PropertyChunks.Remove(m_CurrentChunk);
            }
            
            // Then, if there are no more properties, chunck out sub-containers and items.
            if (m_CurrentChunk == null && m_bDirectory)
            {
                CrawlUrl currentUrl;

				// Next Container or Item.
				currentUrl = m_ContentEnumerator.GetNextContainer();
				if (currentUrl.IsEmpty) // No more containers, try the next item.
					currentUrl = m_ContentEnumerator.GetNextItem();

				if (!currentUrl.IsEmpty) // If we still have an item, process it.
                    m_CurrentChunk = new ChunkInfo(currentUrl.Url, currentUrl.LastModified);
			}
            if (m_CurrentChunk != null)
            {
				pStat.idChunk = ++m_ulChunkId;
				pStat.breakType = CHUNK_BREAKTYPE.CHUNK_EOS;
				pStat.flags = CHUNKSTATE.CHUNK_VALUE;
				pStat.locale = m_lcid;
				pStat.attribute = m_CurrentChunk.FullPropSpec;
				pStat.idChunkSource = 0;
				pStat.cwcStartSource = 0;
				pStat.cwcLenSource = 0;

				hr = (IFilterReturnCodes)SCODE.S_OK;
			}

            Logging.Leave(typeof(Accessor), "GetChunk", hr.ToString());

			return hr;
		}


		public IFilterReturnCodes GetText(ref uint pcwcBuffer, System.IntPtr awcBuffer)
		{
            Logging.Enter(typeof(Accessor), "GetText");
            IFilterReturnCodes hr = IFilterReturnCodes.FILTER_E_NO_TEXT;
			uint cwcBuffer = 0;

            // Should be no need for text chunks, but if there were code would go here.
            
            pcwcBuffer = cwcBuffer;

            Logging.Leave(typeof(Accessor), "GetText", hr.ToString());

			return hr;
		}
	

		public IFilterReturnCodes GetValue(ref IntPtr ppPropValue)
		{
            Logging.Enter(typeof(Accessor), "GetValue");
			IFilterReturnCodes hr = IFilterReturnCodes.FILTER_E_NO_VALUES;

            if (m_CurrentChunk != null)
			{
				string sOperation = "Fetching PropVariant";
				try
				{
					sOperation = "Allocating structure";
                    ppPropValue = Marshal.AllocCoTaskMem(Marshal.SizeOf(m_CurrentChunk.PropVariant));
					sOperation = "Coping structure to unmanaged memory";
                    Marshal.StructureToPtr(m_CurrentChunk.PropVariant, ppPropValue, false);
					hr = (IFilterReturnCodes)SCODE.S_OK;
				}
				catch (Exception e)
				{
					Logging.Exception(sOperation, e);
					hr = IFilterReturnCodes.E_FAIL;
				}
                m_CurrentChunk = null;
			}
			else
				hr = IFilterReturnCodes.FILTER_E_NO_MORE_VALUES;

			Logging.Leave(typeof(Accessor), "GetValue", hr.ToString());
			return hr;
		}


		public IFilterReturnCodes BindRegion(FILTERREGION origPos, ref Guid riid, ref UIntPtr ppunk)
		{
			Logging.Called(typeof(Accessor), "BindRegion");
			return (IFilterReturnCodes)HRESULT.E_NOTIMPL;
		}

		#endregion
	}


    /// <summary>
    /// Holds all the information needed to complete a chunk.
    /// Assumes that each chunk will have exactly one value.
    /// </summary>
    public class ChunkInfo
	{
		public FULLPROPSPEC FullPropSpec; // The property ID of the value we're chunking.
		public PROPVARIANT PropVariant; // The value of the chunk.


        public ChunkInfo(Guid guid, UInt32 propid, object objVal) : this(guid, propid) // Property ID version.
		{
            PropVariant = new PROPVARIANT(objVal);
		}

        public ChunkInfo(Guid guid, string lpwstr, object objVal) : this(guid, lpwstr) // Property Name version.
		{
            PropVariant = new PROPVARIANT(objVal);
        }

        public ChunkInfo(string url, DateTime lastModified) : this(PROPSET_GATHERER.GUID, (UInt32)PROPSET_GATHERER.PID.DIRLINK_WITH_TIME) // URL with date/time.
        {
            PROPVARIANT_URL_VECTOR uv = new PROPVARIANT_URL_VECTOR();

            uv.Url = new PROPVARIANT(url);
            uv.LastModified = new PROPVARIANT(COMInterop.DateTimeToFileTime(lastModified));

            Logging.Trace(string.Format("Adding DIRLINK_WITH_TIME chunk: {0} ({1:D8}{2:D8})", url, uv.LastModified.union.filetime.dwHighDateTime, uv.LastModified.union.filetime.dwLowDateTime));
            PropVariant = new PROPVARIANT(uv);
        }

        private ChunkInfo(Guid guid, UInt32 propid) : this(guid)
        {
            FullPropSpec.psProperty = new PROPSPEC(propid);
        }

        private ChunkInfo(Guid guid, string str) : this(guid)
        {
            FullPropSpec.psProperty = new PROPSPEC(str);
        }
        
        private ChunkInfo(Guid guid)
        {
            FullPropSpec.guidPropSet = guid;
        }
    }
}
